﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using GraphicsLib;
using TowerDefence.LanguageExtensions;

namespace TowerDefence.UI
{
    abstract class UIElement
    {
        const int c_NUM_STICK_FRAMES = 5;
        int stickFrames = 0;

        const float c_TWEEN_SPEED = 0.05f;

        public class StateDef // : ICloneable
        {          
            public Rectangle rect       { get; set; }
            public Point absPoint       { get; set; }  // we work with local coords but require absolute coords for mouse events
            public int padding          { get; set; }
            public int borderSize       { get; set; }
            public Color bgColour       { get; set; }
            public Color imgColour      { get; set; }
            public Color borderColour   { get; set; }
            public Texture2D textureBg  { get; set; }
            public int align            { get; set; }
            // Tried to subclass these into a TextStateDef but it just got messy.
            // As it stands, all components now have knowledge of these components,
            // but only Text components use them for anything.
            public String text          { get; set; }
            public Color fontColour     { get; set; }
            public SpriteFont font      { get; set; }
            public Vector2 textSize     { get; set; }

            public StateDef()
            {               
                this.rect = new Rectangle(0, 0, 0, 0);
                this.absPoint = new Point(0, 0);
                this.borderSize = 0;
                this.padding = 3;
                this.bgColour = Color.White;
                this.borderColour = Color.Black;
                this.imgColour = Color.White;
                this.align = (int)(eAlign.LEFT | eAlign.TOP);
                this.textureBg = null;
                this.text = "";
                this.fontColour = Color.Black;
                this.font = null;
                this.textSize = new Vector2(0,0);                
            }

            public StateDef Copy()
            {
                StateDef copied = new StateDef();
                copied = (StateDef)this.MemberwiseClone();
                copied.rect = this.rect.copy();        
                copied.absPoint = new Point(this.absPoint.X, this.absPoint.Y);
                copied.text = string.Copy(this.text);
                return copied;
            }
        }

        public UIElement parent { protected get; set; }
        public bool bActive { get; set; }

        public enum eAlign
        {
            H_CENTRE =  1,
            LEFT =      1 << 1,
            RIGHT =     1 << 2,
            TOP =       1 << 3,
            BOTTOM =    1 << 4,
            V_CENTRE =  1 << 5
        }

        public enum eState
        {
            NORMAL = 0,
            HIGHLIGHTED,
            SELECTED,
            DISABLED,

            NUM_STATES
        }
        protected StateDef[] stateDefs = new StateDef[(int)eState.NUM_STATES];
        public StateDef curState;

        public StateDef tweenState; // TODO:
        private StateDef tweenTarget;

        public UIElement()
        {
            // setup default values
            this.parent = null;
            for (int i = 0; i < (int)eState.NUM_STATES; ++i)
                stateDefs[i] = null;
            this.bActive = true;
            this.curState = new StateDef();
            stateDefs[(int)eState.NORMAL] = this.curState;
            this.tweenState = null;
        }

        public virtual void AddState(StateDef _def, eState _type)
        {
            this.stateDefs[(int)_type] = _def;
        }

        public virtual void SetState(eState _type)
        {            
            StateDef def = this.stateDefs[(int)_type];

            if (def != null && def != this.curState)
            {
                this.curState = def;
                this.Layout();
                if (parent != null)
                    parent.Layout();                
            }
        }

        public void TweenToState(eState _type)
        {
            Exception up = new Exception("State tweening not implemented.");
            throw up;

            this.tweenState = this.curState.Copy();

            this.curState = this.tweenState;

            this.tweenTarget = stateDefs[(int)_type];
        }

        public StateDef CurrentState()
        {
            return this.curState;
        }

        public virtual void SetAbsoluteOffsets(Point _offset)
        {
            this.curState.absPoint = new Point(
                this.curState.rect.X + _offset.X, 
                this.curState.rect.Y + _offset.Y);
        }

        public virtual void Layout()
        {
            if (this.parent != null)
            {
                this.parent.Layout();
            }
            else
            {
                // because all our layout was done from a bottom-up approach, when we reach the All-Father
                // we traverse back down to find the absolute points. This is to make catching mouse input
                // a piece of piss
                SetAbsoluteOffsets(new Point(0,0));
            }
        }

        public virtual void Update(MouseState _mouseState)
        {
            if (!this.bActive)
                return;

            /************************************************/
            /*  Input
            /************************************************/
            if (tweenState == null)
            {
                if (stickFrames > 0)
                    stickFrames--;

                if (_mouseState.X < this.curState.absPoint.X + this.curState.rect.Width
                    && _mouseState.X > this.curState.absPoint.X
                    && _mouseState.Y > this.curState.absPoint.Y
                    && _mouseState.Y < this.curState.absPoint.Y + this.curState.rect.Height)
                {
                    if (_mouseState.LeftButton == ButtonState.Pressed) // clickety click
                    {
                        //SetState(eState.SELECTED);
                        SetState(eState.SELECTED);

                        stickFrames = c_NUM_STICK_FRAMES;

                        // fire event
                    }
                    else if (stickFrames <= 0)
                        SetState(eState.HIGHLIGHTED);
                }
                else if (stickFrames <= 0)
                {
                    SetState(eState.NORMAL);
                }
            }
            /************************************************/
            /*  Tweening
            /************************************************/
            UpdateTweening();

        }

        private void UpdateTweening()
        {
            /*
             * Too much code can't be arsed, below is for colour only and it doesn't even work yet
            /*
            if (this.tweenState != null)
            {
                Color curCol = tweenState.bgColour;
                Color tarCol = tweenTarget.bgColour;
    
                if (curCol.R > tarCol.R)                
                    curCol.R += (byte)(((float)(tarCol.R - curCol.R)) * c_TWEEN_SPEED);   // mod evaluates to zero, need custom colour floats              
                else if (curCol.R < tarCol.R)                
                    curCol.R += (byte)(((float)(curCol.R - tarCol.R)) * c_TWEEN_SPEED);                     
                else
                    curCol.R = tarCol.R;                

                if (curCol.G > tarCol.G)                
                    curCol.G += (byte)(((float)(tarCol.G - curCol.G)) * c_TWEEN_SPEED);
                else if (curCol.G < tarCol.G)
                    curCol.G += (byte)(((float)(curCol.G - tarCol.G)) * c_TWEEN_SPEED);
                else                
                    curCol.G = tarCol.G;                

                if (curCol.B > tarCol.B)                
                    curCol.B += (byte)(((float)(tarCol.B - curCol.B)) * c_TWEEN_SPEED);
                else if (curCol.B < tarCol.B)
                    curCol.B += (byte)(((float)(curCol.B - tarCol.B)) * c_TWEEN_SPEED);
                else               
                    curCol.B = tarCol.B;                
                
                tweenState.bgColour = curCol;

                if (    curCol.R == tarCol.R
                    &&  curCol.G == tarCol.G
                    &&  curCol.B == tarCol.B)
                {
                    this.curState = tweenTarget;
                    this.tweenState = null;
                }
             
            }
             * */
        }

        public virtual void Draw(Graphics2D _graphics, SpriteBatch _spriteBatch)
        {
            if (! this.bActive)
                return;

            Rectangle offsetRect = this.curState.rect.copy();
            offsetRect.X = this.curState.absPoint.X;
            offsetRect.Y = this.curState.absPoint.Y;

            _graphics.DrawBorderedRectangle(offsetRect, this.curState.borderSize, this.curState.bgColour, this.curState.borderColour, Graphics2D.eBorderType.Internal);

            if (_spriteBatch != null && this.curState.textureBg != null)
            {
                offsetRect.X += this.curState.borderSize;// + this.curState.padding;
                offsetRect.Y += this.curState.borderSize;// + this.curState.padding;
                offsetRect.Width -= (this.curState.borderSize << 1);// + (this.curState.padding << 1);
                offsetRect.Height -= (this.curState.borderSize << 1);// + (this.curState.padding << 1);

                _spriteBatch.Draw(this.curState.textureBg, offsetRect, this.curState.imgColour);
            }
        }
    }

    class Container : UIElement
    {
        protected List<UIElement> children = new List<UIElement>();

        protected Container(Point _p)
        {
            this.curState.rect = new Rectangle(_p.X, _p.Y, 0, 0);
            Layout();
        }

        public override void SetAbsoluteOffsets(Point _offset)
        {
            base.SetAbsoluteOffsets(_offset);

            for (int i = 0; i < children.Count; ++i)
            {
                children[i].SetAbsoluteOffsets(this.curState.absPoint);
            }
        }

        public override void Draw(Graphics2D _graphics, SpriteBatch _spriteBatch)
        {
            if (!this.bActive)
                return;

            base.Draw(_graphics, _spriteBatch);
            
            for (int i = 0; i < children.Count; ++i)
            {
                children[i].Draw(_graphics, _spriteBatch);
            }            
        }

        public override void Update(MouseState _mouseState)
        {
            for (int i = 0; i < children.Count; ++i)
            {
                children[i].Update(_mouseState);
            }
        }

        public void AddChild(UIElement _child)
        {
            children.Add(_child);
            _child.parent = this;
            _child.Layout();
            Layout();
        }

        public override void SetState(UIElement.eState _type)
        {
            base.SetState(_type);

            for (int i = 0; i < children.Count; ++i)
                children[i].SetState(_type);          

        }

        public override void Layout()
        {   
            base.Layout();
        }


        protected void Stretch(int _w, int _h)
        {
            Rectangle newRect = this.curState.rect.copy();

            if (newRect.Height < _h)
                newRect.Height = _h;
            if (newRect.Width < _w)
                newRect.Width = _w;
            this.curState.rect = newRect;

        }
    }

    class VerticalFlowContainer : Container
    {
        public VerticalFlowContainer(Point _p) : base(_p) { }

        public override void Layout()
        {
            int x = this.curState.padding + this.curState.borderSize;
            int y = this.curState.padding + this.curState.borderSize;

            int maxWidth = 0;

            for (int i = 0; i < children.Count; ++i)
            {
                int widthDiff = (this.curState.rect.Width - (this.curState.padding * 2)) - children[i].curState.rect.Width;
                int xMod = 0;
                if ((children[i].curState.align & (int)eAlign.H_CENTRE) != 0)
                {
                    xMod += widthDiff / 2;
                }
                else if ((children[i].curState.align & (int)eAlign.RIGHT) != 0)
                {
                    xMod += widthDiff;
                }

                children[i].curState.rect = new Rectangle(
                    x + xMod,
                    y,
                    children[i].curState.rect.Width,
                    children[i].curState.rect.Height);

                y += children[i].curState.rect.Height;
                y += this.curState.padding;

                if (maxWidth < children[i].curState.rect.Width)
                    maxWidth = children[i].curState.rect.Width;
            }

            y += this.curState.borderSize;
            x += x + maxWidth;

            //Stretch(x, y);
            Rectangle newRect = this.curState.rect.copy();
            newRect.Height = y;
            newRect.Width = x;
            this.curState.rect = newRect;

            base.Layout();
        }
    }

    class HorizontalFlowContainer : Container
    {
        public HorizontalFlowContainer(Point _p) : base(_p) { }

        public override void Layout()
        {
            int x = this.curState.padding + this.curState.borderSize;
            int y = this.curState.padding + this.curState.borderSize;

            int maxHeight = 0;

            for (int i = 0; i < children.Count; ++i)
            {
                int heightDiff = (this.curState.rect.Height - (this.curState.padding * 2)) - children[i].curState.rect.Height;
                int yMod = 0;
                if ((children[i].curState.align & (int)eAlign.V_CENTRE) != 0)
                {
                    yMod += heightDiff / 2;
                }
                else if ((children[i].curState.align & (int)eAlign.BOTTOM) != 0)
                {
                    yMod += heightDiff;
                }

                children[i].curState.rect = new Rectangle(
                    x,
                    y + yMod,
                    children[i].curState.rect.Width,
                    children[i].curState.rect.Height);

                x += children[i].curState.rect.Width;
                x += this.curState.padding;

                if (maxHeight < children[i].curState.rect.Height)
                    maxHeight = children[i].curState.rect.Height;
            }

            x += this.curState.borderSize;
            y += y + maxHeight;
            //Stretch(x, y);
            Rectangle newRect = this.curState.rect.copy();
            newRect.Height = y;
            newRect.Width = x;
            this.curState.rect = newRect;

            base.Layout();
        }


    }

    abstract class AtomicElement : UIElement
    {
    }

    sealed class Image : AtomicElement
    {
        public override void Layout()
        {
            // scale to image size if zero
            if (this.curState.textureBg != null && this.curState.rect.Width == 0 && this.curState.rect.Height == 0)
            {
                Rectangle newRect = this.curState.rect.copy();
                newRect.Width = this.curState.textureBg.Width;
                newRect.Height = this.curState.textureBg.Height;
                this.curState.rect = newRect;
            }

            base.Layout();
        }

        public Image(Texture2D _textureBg) : base()
        {
            this.curState.textureBg = _textureBg;

            Layout();
        }
    }

    sealed class EmptySpace : AtomicElement
    {
        public EmptySpace() : base()
        {
            this.curState.bgColour = Color.TransparentBlack;
        }
    }

    sealed class Text : AtomicElement
    {
        public Text(String _text, SpriteFont _font, Color _fontColour) : base()
        {
            this.curState.text = _text;
            this.curState.font = _font;
            this.curState.fontColour = _fontColour;
                       
            Layout();
        }

        public override void Layout()
        {
            this.curState.textSize = this.curState.font.MeasureString(this.curState.text);
            this.curState.rect = new Rectangle
            (
                0,
                0,
                (int)this.curState.textSize.X + this.curState.padding*2,
                (int)this.curState.textSize.Y + this.curState.padding*2
            );

            base.Layout();
        }

        public override void Draw(Graphics2D _graphics, SpriteBatch _spriteBatch)
        {
            base.Draw(_graphics, _spriteBatch);

            Vector2 pos = new Vector2(this.curState.absPoint.X + this.curState.padding, this.curState.absPoint.Y + this.curState.padding);

            _spriteBatch.DrawString(this.curState.font, this.curState.text, pos, this.curState.fontColour);
        }
    }
}